跳到主要内容

NT 头和 PE 文件

在计算机程序的二进制文件中,尤其是在 Windows 操作系统中,有两个重要的结构:NT 头和PE文件。它们在可执行文件(如 .exe 文件)和动态链接库(.dll 文件)中起着关键作用。

  • NT 头:PE 文件的一部分,包含文件基本信息和加载所需的信息。
  • PE 文件:Windows 操作系统使用的可执行文件格式,定义了程序的结构和内容。

PE 文件

PE 文件(Portable Executable File)是 Windows 可执行文件的格式,加载器在加载可执行文件时,会解析 PE 文件的各个部分,包括 DOS 头、NT 头、节头和各个节的内容。通过解析这些结构,加载器可以确定代码段、数据段、导入表、导出表等内容的位置和大小,并相应地将它们加载到内存中。

PE 文件格式是一种规范,定义了在 Windows 操作系统中如何组织和存储可执行代码和数据。PE 文件结构主要由以下几部分组成:

  1. DOS Header:DOS 头部是PE文件的最前端部分,包含一个兼容旧DOS系统的程序头。如果在非Windows系统上运行,这段代码会打印一条错误信息,提示用户需要Windows才能运行该程序。
  2. NT Headers:紧随DOS头之后,包括Signature、File Header和Optional Header。
  3. Section Headers:节头部,描述了文件中的各个段(section),每个段包含特定类型的数据,如代码段、数据段、资源段等。
  4. Sections:实际的数据段,存储了可执行代码、初始化数据、未初始化数据、导入和导出表、资源等。

当一个 PE 文件被加载时,操作系统执行以下步骤:

  1. 读取 DOS 头

    • DOS 头在文件的最开始部分。它包含一个指向 PE 头的偏移量。
    • 如果文件不是一个有效的 PE 文件(即 PE 头无法找到),加载会失败。
  2. 读取 PE 头(包括 NT 头)

    • 通过 DOS 头中的偏移量找到并读取 PE 头。
    • PE 头包含 File Header 和 Optional Header。File Header 描述了文件的基本信息(如机器类型、节数等),Optional Header 包含了加载和运行时所需的详细信息(如入口点、基地址、内存布局等)。
  3. 读取节头和各个节

    • PE 头后面是节头,每个节头描述了一个节的名称、大小、文件偏移、内存地址等信息。
    • 加载器会根据节头的信息将各个节映射到内存中。
  4. 处理导入表和导出表

    • 导入表描述了该可执行文件依赖的外部函数和库。加载器会根据导入表加载所需的 DLL 并解析函数地址。
    • 导出表描述了该可执行文件可以被其他模块调用的函数和数据。
  5. 初始化和执行

    • 加载器完成所有必要的映射和解析后,会调用可执行文件的入口点(在 Optional Header 中指定),从而开始程序的执行。

通过以上步骤,操作系统能够正确地加载和执行 PE 文件。PE 文件格式的灵活性和扩展性使得它能够支持多种不同的应用场景,包括可执行文件、动态链接库、驱动程序等。

示例:加载 DLL

假设我们有一个名为 example.dll 的动态链接库,并且我们要在一个程序中动态加载它并调用其中的一个函数。

以下是一个 C++ 程序,用于动态加载 example.dll 并调用其中的 ExampleFunction 函数:

#include <windows.h>
#include <iostream>

typedef void (*ExampleFunctionType)();

int main() {
// 加载 DLL
HMODULE hModule = LoadLibrary(L"example.dll");
if (!hModule) {
std::cerr << "Failed to load DLL." << std::endl;
return 1;
}

// 获取函数指针
ExampleFunctionType ExampleFunction = (ExampleFunctionType)GetProcAddress(hModule, "ExampleFunction");
if (!ExampleFunction) {
std::cerr << "Failed to get function address." << std::endl;
FreeLibrary(hModule);
return 1;
}

// 调用函数
ExampleFunction();

// 释放 DLL
FreeLibrary(hModule);

return 0;
}

上面的 PE 文件加载过程,当调用 LoadLibrary 加载 example.dll 时,操作系统会执行以下步骤:

  1. 读取文件头

    • 操作系统会首先读取 DLL 文件的 DOS 头和 PE 头。
    • DOS 头包含一个指向 PE 头的偏移量。
  2. 解析 PE 头

    • 操作系统使用 DOS 头中的偏移量找到并读取 PE 头,验证其合法性。
    • PE 头包含 File Header 和 Optional Header,其中包括了入口点、节数、基地址等信息。
  3. 映射节到内存

    • 根据 PE 头中的节头信息,操作系统将 DLL 文件的各个节映射到进程的地址空间。
    • 通常,节包括代码段(.text)、数据段(.data)、导入表(.idata)、导出表(.edata)等。
  4. 解析导入表

    • 导入表(.idata 节)列出了 DLL 所依赖的其他模块和函数。操作系统会根据导入表加载这些依赖模块,并解析它们的地址。
    • 例如,如果 example.dll 依赖于 kernel32.dll 中的某个函数,操作系统会确保 kernel32.dll 已加载,并将相应函数的地址填入导入表中。
  5. 调用入口点

    • 如果 DLL 有一个入口点(通常是 DllMain 函数),操作系统会在加载 DLL 后调用它。
    • DllMain 函数可以执行初始化任务,如分配资源或设置全局变量。
  6. 准备使用

    • 到这一步,DLL 已经完全加载并初始化,可以供调用方使用。

示例中的工作流程

  1. LoadLibrary

    • LoadLibrary(L"example.dll") 会触发上述的加载过程。操作系统读取和解析 example.dll 的 PE 头,将其映射到内存中,解析导入表,并调用入口点(如果有的话)。
  2. GetProcAddress

    • GetProcAddress(hModule, "ExampleFunction") 查找并返回 ExampleFunction 函数的地址。
    • 操作系统会在 DLL 的导出表(.edata 节)中查找 ExampleFunction 的地址。
  3. 调用函数

    • 调用 ExampleFunction 函数时,程序会跳转到 example.dll 中对应的地址并执行函数的代码。
  4. 释放 DLL

    • FreeLibrary(hModule) 释放 DLL。操作系统会减少 DLL 的引用计数,当引用计数为 0 时,操作系统会卸载 DLL 并释放相关资源。

NT 头

NT 头(NT Header)是指在 Windows 可执行文件中用于描述文件格式的结构。NT 头包含了文件的整体信息和加载所需的信息。操作系统或加载器在加载可执行文件时,会解析 NT 头以获取文件的基本属性、入口点、内存布局等重要信息。这些信息使得操作系统能够正确地将可执行文件映射到内存并开始执行。

它位于可执行文件的头部,是 PE 文件头的一部分。NT 头包含关于文件本身的信息,包括其结构和加载方式。NT 头的主要组成部分包括:

  1. Signature:一个4字节的魔术数字,通常是 "PE\0\0",用来标识文件是一个PE文件。
  2. File Header:包含文件的整体信息,例如目标机器类型、文件中节的数量、时间戳等。
  3. Optional Header:包含程序加载时需要的各种信息,如入口点地址、代码段和数据段的大小和地址、需要的内存大小等。

可以使用 C++ 代码来读取 NT 头的信息,例如:

PIMAGE_NT_HEADERS ntHeaders = RtlImageNtHeader(moduleData);
if (!ntHeaders) {
std::cerr << "Failed to get NT headers from module data." << std::endl;
VirtualFree(moduleData, 0, MEM_RELEASE);
CloseHandle(targetProcess);
return GetLastError();
}

std::cout << "Signature: " << std::hex << ntHeaders->Signature << std::endl;
std::cout << "Machine: " << std::hex << ntHeaders->FileHeader.Machine << std::endl;
std::cout << "Number of Sections: " << std::dec << ntHeaders->FileHeader.NumberOfSections << std::endl;
std::cout << "Time Stamp: " << std::dec << ntHeaders->FileHeader.TimeDateStamp << std::endl;
std::cout << "Entry Point: " << std::hex << ntHeaders->OptionalHeader.AddressOfEntryPoint << std::endl;

这个 RtlImageNtHeader(moduleData) 这是一个 Windows API 函数,用于获取指定模块数据的 NT 头。上面的 moduleData 是指向模块数据的指针(通常是一个映射到内存中的 PE 文件)。